#!/usr/bin/perl -w 

my $VERSION			= "1.03 2009-09-19.01";
my $DEBUG 			= 0;		# 0 - errors, 1 - all
my $CONFIG 			= "/etc/spineld.conf";	# config file
my $READ_TIMEOUT 	= 1;		# read timeout in seconds (used when a device doesn't response)
my $STATFILE 		= "/var/spool/spineld/values.stat";
#my $VALID_DELAY		= 30;		# how long will be obtained value considered as valid (solve read errors)
my $VALID_DELAY		= 60;		# how long will be obtained value considered as valid (solve read errors)
my $LOGNAME        = substr($0, rindex($0, "/") + 1);;
my $LOGFACILITY    = "daemon.info";
my $DATA;					# structure for obtained data format 
                            # $DATA->{"key"}->{"ip"}		# ip address of the GNOME 485
                            # $DATA->{"key"}->{"addr"}	# RS 485 address
                            # $DATA->{"key"}->{"chan"}	# channel 
                            # $DATA->{"key"}->{"raw_value"}   # value obtained from a device
                            # $DATA->{"key"}->{"value"}   # final value (= raw_value * koef_a + koef_b)
                            # $DATA->{"key"}->{"valid"}   # flag if value is valid
                            # $DATA->{"key"}->{"expr"}    # conversion expression
                            # $DATA->{"key"}->{"descr"}   # text description
                            # $DATA->{"key"}->{"updated"}   # timestamp of the last item update
							#           
my $STATS;					# statistics info per a device
							# $STATS->{"ip:port, 0x<addr>}->{"requestes"} - total number of read requests
							# $STATS->{"ip:port, 0x<addr>}->{"err_noresp"} - error - no response to an answer
							# $STATS->{"ip:port, 0x<addr>}->{"err_packets"} - error - total number of error packets
							# $STATS->{"ip:port, 0x<addr>}->{"reset_ts"} - the timestamp of last counters reset
							# $STATS->{"ip:port, 0x<addr>}->{"update_ts"} - the timestamp of the last update
my $EXPR;					# description of expressions for computed values
							# $EXPR->{"key"} = <expression>
my %OPTS;

use strict;
use IO::Socket;
use Data::Dumper;
use File::Basename;
use Time::HiRes qw(usleep ualarm);
use POSIX qw(strftime setsid);
use Getopt::Std;
use Sys::Syslog qw(:DEFAULT setlogsock);



# logging
sub mylog {
	my ($msg, @par) = @_;
	my $lmsg = sprintf($msg, @par);
	if ($DEBUG > 0) {
		printf "%s[%d]: %s\n", strftime("%Y-%m-%d.%H:%M:%S", localtime), $$, $lmsg;
	}
	setlogsock('unix');
	openlog("$LOGNAME\[$$\]", 'ndelay', 'user');
	syslog($LOGFACILITY, $lmsg);
}

# demonizace process
sub daemonize() {
	chdir '/'					or die "Can't chdir to /: $!";
	open STDIN, '/dev/null'		or die "Can't read /dev/null: $!";
	open STDOUT, '>/dev/null'	or die "Can't write to /dev/null: $!";
	defined(my $pid = fork)		or die "Can't fork: $!";
	exit if $pid;
	setsid						or die "Can't start a new session: $!";
	open STDERR, '>&STDOUT'		or die "Can't dup stdout: $!";
}

# cgange user
sub chuser($) {
	my ($user) = @_;

	my ($login,$pass,$uid,$gid) = getpwnam($user);
    # gid must be changed before uid. at least on my computer :)
	$( = $gid;
	$) = $gid;
	$< = $uid;
	$> = $uid;

	## Check that we managed to change Group/User IDs properly...
	## Change warn to die if it's important to you
	if (  ((split(/ /,$)))[0] ne $gid) || ((split(/ /,$())[0] ne $gid)  ) {
		warn "Couldn't Change Group ID!\n";
	}

	if (  ($> ne $uid) || ($< ne $uid)  ) {
		mylog("Couldn't Change User ID!\n");
		die "Couldn't Change User ID!\n";
	}

	mylog("Going to user %s (uid: %d, gid: %d)", $user, $uid, $gid);

	## We don't need these anymore...
	undef($login);
	undef($pass);
	undef($uid);
	undef($gid);

	# and so the program will actually RUN at this user:
	fork and wait and exit;
}



# print string in hex format 
# arg: prefix, string
sub format_hex($) {
	my ($data) = @_;
	my $res = "";

	foreach (split(//, $data)) {
		$res .= sprintf "%02x ", ord("$_");
	}
	return $res;
}

# compute checksum for spinel packet
sub sp_cksum($) {
	my ($data) = @_;

	my $sum = 0xFF;				
	for (split(//, $data)) {
		my ($num) = unpack("C", $_);
		$sum -= $num;
	}
	return pack("C", $sum & 0xFF);
}

sub hexnum($) {
	my ($a) = @_;

	if ($a =~ /0x\d*/) {
		$a = hex($a);
	}
	return $a;
}

# parse config file
sub parse_config() {

    if ( ! -f $CONFIG ) {
        mylog("Config file %s not found\n", $CONFIG);
        exit 1;
    }
    open F1, "< $CONFIG";
    my $line;
	my $cnt = 0;
    while (<F1>) {
        chomp ;
        # join multiple lines
        if (/(.+)\\$/) {
            $line .= "$1 ";
            next;
        } else {
            $line .= $_;
        }
        # remove comments
        ($line) = split(/#/, $line);
        if (defined($line) && $line ne "") {
            my ($type, $key, $line) = split(/\s+/, $line, 3);
			if ($type eq "source") {
            	my ($ip, $addr, $channel, $expr) = split(/\s+/, $line, 4);
            	$DATA->{$key}->{"ip"} = $ip;
            	$DATA->{$key}->{"addr"} = hexnum($addr);
            	$DATA->{$key}->{"channel"} = hexnum($channel);
            	$DATA->{$key}->{"expr"} = $expr;
            	$DATA->{$key}->{"line"} = $cnt++;
			} elsif ($type eq "expr") {
				$EXPR->{$key} = $line;
			}
        }
        $line = "";
    }
}

# store status file
sub load_stat() {
	my $dir = dirname($STATFILE);
	if ( ! -d  $dir ) {
		mylog("ERROR: Directory %s not found!\n", $dir);
		exit 1;
	}
	if ( -f $STATFILE ) {
		open F1, "< $STATFILE";
		my $data = join("", <F1>);
		my $VAR1;
		eval($data);
		$DATA = undef;
		$DATA = $VAR1->{'DATA'};
		$STATS = $VAR1->{'STATS'};
		close F1;
	}
}

# store status file
sub store_stat() {
	my $mdata = {'DATA' => $DATA, 'STATS' => $STATS};
	open F1, "> $STATFILE.$$";
	print F1 Dumper($mdata);
	close F1;
	rename("$STATFILE.$$", "$STATFILE") || mylog("Can't update or rename file %s", $STATFILE);
}



# apply conversion function 
# arg: value
#      eval expr (X will be replaced by value
#      ex.: 4*X+10
sub eval_val($$) {
	my ($x, $expr) = @_;

	return $x if (!defined($expr));

	$expr =~ s/X/$x/i;
	$expr =~ s/%{X}/$x/i;

	if ($expr =~ /^(.+)$/) {
		$expr = $1;
	}

	my $y = eval $expr;

	return $y; 
}

# check data received by the server and extract data from packet
# arg: data received as a response from a sensor
# return: data field part extracted from the packet
# i       undef  when data can't be extracted 
sub sp_extract_data($) {
	my ($pkt) = @_;

	return undef if (!defined($pkt) || $pkt eq "");

	my ($pre, $frm, $len, $addr, $sig, $ack) = unpack("CCnCCC", $pkt);

	# check pre and frm 
	return undef if (!defined($pre) || $pre != 0x2a);			# 
	return undef if (!defined($frm) || $frm != 0x61);			# format
	return undef if (!defined($ack) || $ack != 0x00);			# ack 
	return undef if (unpack("C", substr($pkt, - 1)) != 0x0d);	# 0x0d  at the end of the packet

	my $data = substr($pkt, 7, length($pkt) - 7 - 2);
	my $csum = sp_cksum(substr($pkt, 0, length($pkt) - 2));
	my $psum = substr($pkt, length($pkt) - 2, 1);

	return undef if ($csum ne $psum); 

	return $data;
}


# formatuje pozadavek v protokolu spinel
# prvni tri argumenty se zadaveji jako ciselne hodnoty 
# tj. napr pro adresu muzeme pouzit tyto ekvivalentni formaty # 49, 0x31, '1' 
# stejny mechanizmus plati i pro sig, inst
# data je retezec kde kazdy znak reprezentuje jeden byte
# arg: adresa, sig, inst, data
# priklad: 
sub sp_prepare_query($$$$) {
	my ($addr, $sig, $inst, $udata) = @_;
	my $res = '';	# result packet

	my $udatalen = defined($udata) ? length($udata) : 0;

	# packet formating
	$res .= pack("C", 0x2a);		# PRE (2B)
	$res .= pack("C", 0x61);		# FRM (2B)	
	$res .= pack("n", $udatalen + 5 ); 		# packet length (4B)
	$res .= pack("C", $addr);		# device addrss (2B)
	$res .= pack("C", $sig);		# signature (user defined number for completing response) (2B)
	$res .= pack("C", $inst);		# instruction (2B)
	$res .= $udata if (defined($udata));	# data (nB)
	$res .= sp_cksum($res);			# suma
	$res .= pack("C", 0x0d); 		# CR
	
}

# prepare query and wait for the answer 
sub sp_query($$$$$$) {
	my ($sock, $addr, $sig, $inst, $udata, $utimeout) = @_;

	my $data = sp_prepare_query($addr, $sig, $inst, $udata);
	mylog(" SND: %s", format_hex($data)) if ($DEBUG);

	# send data to the sensor
	usleep(10 * 1000);		# sleep 10ms 

	my $buff = "";
	my $limit = 1000;
	my $ch = '';
	my $timeout = 0;

	# read data until 0x0d is sent or number of bytes is bigger than 1000	
	# we'll use special eval code to read with timeout
	print $sock $data; 

	while ($limit-- > 0) {
		eval {
			local $SIG{ALRM} = sub { die "alarm\n" }; # NB: \n required
			alarm $utimeout;
			my $nread = sysread($sock, $ch, 1);
			alarm 0;
		};
		last if ($@ eq "alarm\n");
#		printf "# %x %d %s -\n", unpack("C", $ch), $timeout, $@;
		# check if the packet has been already received
		$buff .= $ch;
		last if (defined(sp_extract_data($buff)));
	}

	mylog(" RCV: %s", format_hex($buff)) if ($DEBUG);
	my $rdata = sp_extract_data($buff);
	mylog("Received invalid packet for send '%s', received '%s'", 
		format_hex($data), format_hex($buff)) if (!defined($rdata));

	return $rdata;
}

# load data from sensor through 0x51 instruction and fill into $OUTPUT structure
# par: socket with open connection to RS485 converter
#      addr: device address 
sub sp_get_values($$$) {
	my ($sock, $ip, $addr) = @_;
	my ($data) = undef;

	# TQS3 use different instruction for data reading. 
	my $skey = sprintf("%s, 0x%02x", $ip, $addr);

	if (!defined($STATS->{$skey}->{"info"}) || $STATS->{$skey}->{"info"} =~ /TQS3/) {
		$data = sp_query($sock, $addr, 0x0, 0x51, undef, $READ_TIMEOUT);
	} else {
		$data = sp_query($sock, $addr, 0x0, 0x51, pack("C", 0x0), $READ_TIMEOUT);
	}

	return 0 if (!defined($data) || length($data) < 7);

#	print_hex("$ip, $addr DBG:", $data);

	while (defined($data) && length($data) > 0) {
		my ($chan, $stat, $val, $xdata) = unpack("CCna*", $data);
		$data = $xdata;
		$stat = $stat>>7 & 0x1;
		# find items in $DATA where raw data should be stored
		foreach my $key (keys %{$DATA}) {
			next if ($DATA->{$key}->{"ip"} ne $ip);
			next if ($DATA->{$key}->{"addr"} != $addr);
			next if ($DATA->{$key}->{"channel"} != $chan);
			$DATA->{$key}->{"raw_value"} = $val;
			$DATA->{$key}->{"valid"} = $stat;
			$DATA->{$key}->{"updated"} = time();
			$DATA->{$key}->{"value"} = eval_val($val, $DATA->{$key}->{"expr"});
		}
	#	print_hex("xx: ", $data);
	}
	return 1;
}

# load number of communication errors through 0xf4 instruction and return 
# par: socket with open connection to RS485 converter
#      addr: device address 
sub sp_get_errors($$) {
	my ($sock, $addr) = @_;

	my $data = sp_query($sock, $addr, 0x0, 0xF4, undef, $READ_TIMEOUT);
	if (defined($data)) {
		return unpack("C", $data);
	} else {
		return undef;
	}
}

# get version through (0xf3) and serial number (0xf3)  and return 
# par: socket with open connection to RS485 converter
#      addr: device address 
sub sp_get_info($$) {
	my ($sock, $addr) = @_;
	my ($version, $serial, $product, $other) = ("?", "?", "?", "?");

	my $data = sp_query($sock, $addr, 0x0, 0xF3, undef, $READ_TIMEOUT);
	if (defined($data)) {
		$version = unpack("a*", $data);
	} else {
		return undef;
	}

	$data = sp_query($sock, $addr, 0x0, 0xFA, undef, $READ_TIMEOUT);
	if (defined($data)) {
		($product,  $serial, $other) = unpack("nna*", $data);
		$product = sprintf("%04d", $product);
		$serial = sprintf("%04d", $serial);
	} else {
		return undef;
	}
	
	# concatenate serial number and product version
	my @arr = split(/ /, $version);
	foreach my $x (0 .. @arr - 1) { 
		if ($arr[$x] =~ /v$product(.+);/) {
			$arr[$x] = sprintf("v%s%s/%s;", $product, $1, $serial);
		}
	}
	#return sprintf("%s; SN: %s/%s", $version, $product, $serial);
	return join(" ", @arr);;
}

# reset device through 0xe3 instruction and return 
# par: socket with open connection to RS485 converter
#      addr: device address 
sub sp_reset($$) {
	my ($sock, $addr) = @_;

	sp_query($sock, $addr, 0x0, 0xE3, undef, $READ_TIMEOUT);
	sleep(2);
}

# take data from $DATA, group by IP address:port, open 
# socket and  read data from devices
sub load_all_sensors() {

	my %ips; 

	# split up data into groups by ip address
	foreach my $key (keys %{$DATA}) {
		my $ip = $DATA->{$key}->{"ip"};
		my $addr = $DATA->{$key}->{"addr"};
		$ips{$ip}->{$addr} = 1;
	}

	my $totalerors = 0;
	my $totalnocom = 0;

	# walk through ip address
	foreach my $ip (keys %ips) {
		my $sock = new IO::Socket::INET (PeerAddr => $ip, Proto => 'tcp' ); 
		next if (!$sock);
			
		$sock->autoflush(1);
		foreach my $addr (sort { $a <=> $b } keys %{$ips{$ip}}) {

			mylog("Reading %s, 0x%0x", $ip, $addr) if ($DEBUG);
			my $skey = sprintf("%s, 0x%02x", $ip, $addr);

			my $info = sp_get_info($sock, $addr);

			# set device info
			if (!defined($STATS->{$skey}->{"info"}) || $STATS->{$skey}->{"info"} ne $info) {
				$STATS->{$skey}->{"info"} = $info;
				mylog("Device found on %s, 0x%x : %s", $ip, $addr, $info);
			}

			my $retcode = sp_get_values($sock, $ip, $addr);
#			my $version = sp_get_version($sock, $addr);
			my $errors = sp_get_errors($sock, $addr);

			# update statistics
			if (defined($STATS->{$skey}->{"requests"})) {
				$STATS->{$skey}->{"requests"}++;
			} else {
				$STATS->{$skey}->{"requests"} = 1;
			}

			#reset device if the errors was detect and device is TQS3 
#			if ($info =~ /TQS3/ && $errors == 8) {
#				$errors = 0;
#			}

			# update statistics
			$errors = defined($errors) ? $errors : 0;
			if (defined($STATS->{$skey}->{"err_packets"})) {
				$STATS->{$skey}->{"err_packets"} += $errors;
			} else {
				$STATS->{$skey}->{"err_packets"} = $errors;
			}

			if (!defined($STATS->{$skey}->{"err_noresp"})) {
				$STATS->{$skey}->{"err_noresp"} = 0;
			}
			$STATS->{$skey}->{"err_noresp"}++ if (!$retcode);
			
			if (!defined($STATS->{$skey}->{"reset_ts"})) {
				$STATS->{$skey}->{"reset_ts"} = time();
			}

			if (!defined($STATS->{$skey}->{"ok_update_ts"})) {
				$STATS->{$skey}->{"ok_update_ts"} = time();
			}

			if (!defined($STATS->{$skey}->{"fail_update_ts"})) {
				$STATS->{$skey}->{"fail_update_ts"} = time();
			}

			$STATS->{$skey}->{"ok_update_ts"} = time() if ($retcode);
			$STATS->{$skey}->{"fail_update_ts"} = time() if (!$retcode);
			$STATS->{$skey}->{"update_ts"} = time();
			$STATS->{$skey}->{"last_ok"} = $retcode;

			mylog("Read have done %s, 0x%0x: errors=%d, validdata=%d", $ip, $addr, $errors, $retcode) 
			if ($DEBUG || $retcode == 0 || $errors > 0);
		}
		close($sock);
	}
}

# print statistics
sub print_stats() {

	my $res = "";
	my $lastip = "";
	
	foreach my $skey (sort keys %{$STATS}) {
		my ($ip, $addr) = split(/,/, $skey);
		if ($ip ne $lastip) {
			$res .= sprintf("%s\n", $ip);
			$lastip = $ip;
		}
		my  $updtime = $STATS->{$skey}->{"update_ts"};
		$updtime -= $STATS->{$skey}->{"last_ok"} ? 
					$STATS->{$skey}->{"fail_update_ts"} : $STATS->{$skey}->{"ok_update_ts"};
		$res .= sprintf "  %s  %9ds %9d %9d %9d    %s\n", 
			$addr,
			$STATS->{$skey}->{"update_ts"} - $STATS->{$skey}->{"reset_ts"},
			$STATS->{$skey}->{"requests"},
			$STATS->{$skey}->{"err_noresp"},
			$STATS->{$skey}->{"err_packets"},
			$STATS->{$skey}->{"info"};
	}

	if ($res ne "") {
		#printf strftime("%Y-%m-%d.%H:%M:%S\n", localtime());
		printf "   Aaddr  SinceStart   Requesets  NoReply   ErrPkts LastState\n";
		print $res;
		print "\n";
	}

}

sub get_value($);
sub get_value($) {
	my ($key) = @_;

	my $val = undef;

	if (defined($EXPR->{$key})) {  #we found expression, so we try to found variables and eval it 
		# varables are enclosed by % % 
		my $expr = $EXPR->{$key};


		while ($expr =~ /(.*)%{(.+?)}(.*)/) {	
			my ($p1, $p2, $p3) = ($1, $2, $3);
			$val = get_value($p2);
			if (defined($val)) {
				$expr = $p1.get_value($p2).$p3;
			} else {
				return undef;
			}
		}
		$expr =~ s/%%/%/g;
		my $res = eval $expr;	
		printf "REP: %s -> %s = %s \n", $EXPR->{$key}, $expr, $res if ($DEBUG);
	
		return $res;	
	}

	if (defined($DATA->{$key}->{"value"}) && defined($DATA->{$key}->{"updated"})) {
		if ($DATA->{$key}->{"updated"} + $VALID_DELAY > time()) {
			$val = $DATA->{$key}->{"value"};
		}
	}
	return $val;
}

# print values
sub print_values() {

	foreach (sort { $DATA->{$a}->{"line"} <=> $DATA->{$b}->{"line"} } keys %{$DATA}) {
		my $val =  get_value($_); 
		printf "source    %-15s : %s\n", $_, defined($val) ? $val : '  ?  ';
	}
	foreach (sort keys %{$EXPR}) {
		my $val =  get_value($_); 
		printf "expr      %-15s : %s\n", $_, defined($val) ? $val : '  ?  ';
	}
}

# usage help
sub usage_spinel() {
	printf "Usage:\n\n";
	printf " spinel [ -o a | s ] | <key>  \n\n"; 
	printf "   -oa  : print all values enquired by spineld\n";
	printf "   -os  : print serial numbers of connected devices and communication counters\n";
	printf "  <key> : return data for the key\n\n";
	printf "Example:\n\n";
	printf "  spinel -sa \n\n";
	exit 1;
	
}

sub usage_spineld() {
	printf "spineld, version %s\n\n", $VERSION;
	printf "Usage:\n\n";
	printf " spineld [ -u -d ]\n";
	printf "  -u <user>    : switch to user <user>\n";
	printf "  -d <level>   : set debug level (default: 0) and stay in foreground\n\n";
	exit 1;
	
}


# main body
$SIG{CHLD} = sub { wait(); };

if (!getopt("d:u:q:o:", \%OPTS) || defined($OPTS{"?"})) {
	usage_spineld();
	exit 1;
}

if (defined($OPTS{"d"})) {
	$DEBUG = $OPTS{"d"};
}

# the client code part (spinel)
if ($0 =~ /.*\/get_spinel/ || $0 =~ /.*\/spinel$/) {

	parse_config();
	load_stat();
	if (defined($OPTS{"o"}) && $OPTS{"o"} eq "a") {
		print_values();
		exit 0;
	}
	if (defined($OPTS{"o"}) && $OPTS{"o"} eq "s") {
		print_stats();
		exit 0;
	}
	
	#print_stats();
	my $val = get_value($ARGV[0]);
	if (defined($val)) {
		printf "%s\n", $val;
		exit 0;
	} else {
		printf "\n";
		exit 2;
	}
	usage_spinel();
	exit 1;
}

# the daemon mode part
if (defined($OPTS{"u"})) {
	chuser($OPTS{"u"});
}

if ($DEBUG == 0) {
	daemonize();
}
mylog("Server started with debug level %d, version %s.", $DEBUG, $VERSION);
for (;;) {
	parse_config();
	load_all_sensors();
	store_stat();
	sleep(1);
}

1;

__END__
=head1 NAME

spineld - daemon which collects data from sensors using the spinel protocol
spinel  - user interface to show collected data by spineld

=head1 SYNOPIS

spineld [OPTION] 
spinel  [OPTION] [<item>]


=head1 DESCRIPTION

This daemon allows load data from devices with spinel protocol 
support(http://www.papouch.com/shop/scripts/_spinel.asp). This 
protocol is supported by almost measure boards and other equipments 
produced by Papouch s.r.o. (http://www.papouch.com/en/). The daemon 
periodically gains data from configured sensors and provides output 
for other system. There also client interface which allows user to 
display data collected by the daemon. The system also provides 
interface to zabbix monitoring system. 


=head1 OPTIONS

spineld -u <user> -d <level> 
 
   -u   - switch daemon to the <user> after start
   -d   - debug level; with the debug level > 0 the daemon will stay in foreground

spinel [ -a o|s ] | [ <item> ]
  
   -ao   - display values of the all items defined in the config file
   -as   - display statistics regarding to the spinel devices
  <item> - display value; if the value is not valid an empty line is returned 

=head1 HOMEPAGE, DOWNLOAD

Homepage: http://code.google.com/p/spineld/
Download: http://code.google.com/p/spineld/downloads/list


=head1 INETGRATION WITH ZABBIX MONITORING SYSTEM

Zabbix (http://www.zabbix.com)) is huge monitoring system. Spineld have extensions which
allows zabbix server to get data from the devices. For this purpose you can use 
spinel command. The follow example shows how to proceed to carry out integration with 
the zabbix system.

1. Install the spineld package into your system.
2. Configure device in /etc/spineld.conf and run spinel daemon (/etc/init.d/spineld start)
3. Check if data are downloaded correctly (spinel -oa, spinel -os)
4. If the directory /etc/zabbix/externalscripts/ create link to spinel command 
    (ln -s /usr/bin/spinel /etc/zabbix/externalscripts/spinel)
5. Connect into zabbix web interface and go to "Configuration" -> "Items" and create a new one. 
6. Into the key item fill in the follow string:
   spinel[<item>] 
  other items fill in as you need. 


=head1 CONFIG FILE

Spineld used the text based config file. Each line describes one measured value. As the 
parameter of the line device ip adders, spinel address and channel number are required. 
There are also possibility to have got a special values which are evaluated from 
the basic (source) values. For more detailed description of the config  file, please
see into distribution spineld.conf (/etc/spineld.conf) file. 

Examples of the lines in the config:

 source pwr_1         147.229.255.122:10001     0x1    1     x/300          # AC source 1
 source pwr_2         147.229.255.122:10001     0x1    2     x/300          # AC source 2

 expr  sum_1_2       %{pwr_1} + %{pwr_2}									# Sum 


=head1 CHANGES
 - 2009-09-03: Initial release
 - 2009-09-05: Several bugs removed 
 For additional changes see http://code.google.com/p/spineld/source/list


=head1 AUTHOR

Tomas Podermasnki at Brno University of Technology, <tpoder@cis.vutbr.cz>


=head1 REPORTING BUGS

Bugs please report directly to the author.


=head1 COPYRIGHT

This is free software. You may redistribute copies of it under the terms of the
GNU General Public License <http://www.gnu.org/licenses/gpl.html>.


=head1 EXTERNAL LINKS

Spinel protocol: 
   http://papouch.com/shop/scripts/_spinel.asp

A devices which support spinel protocol (measure devices, thermometers, ...): 
http://papouch.com/shop/scripts/_list.asp?kat=4&pages=1
http://papouch.com/shop/scripts/_list.asp?kat=3&pages=1
http://www.papouch.com/en/products.asp?dir=measuring
http://www.papouch.com/en/products.asp?dir=thermometers
 

